# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import copy
import json
import logging
import math
from typing import List, Optional, Union

import requests
from recordclass import recordclass
from tornado import gen
from tornado.ioloop import PeriodicCallback

from coin.base.config import Config
from coin.base.param_util import to_list
from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_gateway_ready import PositionOrderGatewayReady
from coin.exchange.base.order_fill_util import FillManager
from coin.exchange.base.order_gateway import (BaseOrderGatewayInfoMutator,
                                              Order,
                                              OrderGatewayBase,
                                              OrderGatewayStatus,
                                              OrderSide,
                                              OrderType)
from coin.exchange.base.order_gateway_logger import gen_og_log_request
from coin.exchange.base.order_id_util import OrderIdGenerator
from coin.exchange.okex.kr_rest.currency import OkexCurrency
from coin.exchange.okex.kr_rest.private_client import OkexPrivateParser
from coin.exchange.okex.kr_rest.product import OkexProduct
from coin.exchange.okex.order_gateway_log import OkexOrderGatewayLogProvider
from coin.exchange.okex.rest.trade import OkexRestTrade
from coin.exchange.okex.ws.trade import OkexSpotTradeClient
from coin.exchange.util.rest_rate_limit import RestRateLimiter
from coin.proto.coin_order_gateway_pb2 import (OrderEvent, OrderGatewayConfig)
from coin.proto.coin_query_pb2 import AccountBalance
from coin.strategy.hk_hard_arb.og_util import print_working_orders

OkexOrderInternal = recordclass('OkexOrderInternal',
                                [
                                    'timestamp',
                                    'exchange_order_id',
                                    'is_external_order',
                                    'fill',
                                    'fully_filled',
                                    'cancel_confirmed',
                                    'cancel_sent_time',
                                    'status',
                                    'update_received_timestamp',
                                    'fee',
                                ])


def create_okex_order_internal(*,
                               timestamp=None,
                               exchange_order_id=None,
                               is_external_order=False,
                               fill=None,
                               fully_filled=False,
                               cancel_confirmed=False,
                               cancel_sent_time=None,
                               status=None,
                               update_received_timestamp=None,
                               fee=0):
  return OkexOrderInternal(
      timestamp=timestamp,
      exchange_order_id=exchange_order_id,
      is_external_order=is_external_order,
      fill=fill,
      fully_filled=fully_filled,
      cancel_confirmed=cancel_confirmed,
      cancel_sent_time=cancel_sent_time,
      status=status,
      update_received_timestamp=update_received_timestamp,
      fee=fee,
  )


def _compare_order(order_1, order_2):
  return (order_1.product == order_2.product and math.isclose(order_1.price, order_2.price)
          and math.isclose(order_1.qty, order_2.qty) and order_1.order_side == order_2.order_side
          and order_1.order_type == order_2.order_type)


def create_okex_order_from_traderecord(data, timestamp):
  assert 'symbol' in data, data
  native_symbol = data['symbol']
  product = OkexProduct.FromStrNativeProduct(native_symbol)

  exchange_order_id = int(data['orderId'])
  price_orig = float(data['tradeUnitPrice'])  # for limit order
  qty_orig = float(data['tradeAmount'])
  qty_accum_fill = float(data['completedTradeAmount'])
  price_avg_fill = float(data['averagePrice'])

  order_side = OrderSide.BUY if data['tradeType'] == 'buy' else OrderSide.SELL
  order_type = OrderType.LIMIT

  status = int(data['status'])
  fill = FillManager(price_orig=price_orig,
                     qty_orig=qty_orig,
                     qty_accum_fill=qty_accum_fill,
                     price_avg_fill=price_avg_fill)
  internal = create_okex_order_internal(
      exchange_order_id=exchange_order_id,
      update_received_timestamp=timestamp,
      fill=fill,
      status=status,
  )
  order = Order(order_id=None,
                product=product,
                price=price_orig,
                qty=qty_orig,
                order_side=order_side,
                order_type=order_type,
                tag=None,
                accepted=True,
                cancel_sent=False,
                internal=internal)
  return status, order


def create_okex_order_from_orderinfo(data, timestamp):
  assert 'symbol' in data, data
  native_symbol = data['symbol']
  product = OkexProduct.FromStrNativeProduct(native_symbol)

  exchange_order_id = int(data['order_id'])
  price_orig = float(data['price'])
  price_avg_fill = float(data['avg_price'])
  qty_orig = float(data['amount'])
  qty_accum_fill = float(data['deal_amount'])
  order_side = OrderSide.BUY if data['type'] == 'buy' else OrderSide.SELL
  order_type = OrderType.LIMIT

  status = int(data['status'])
  fill = FillManager(price_orig=price_orig,
                     qty_orig=qty_orig,
                     qty_accum_fill=qty_accum_fill,
                     price_avg_fill=price_avg_fill)
  internal = create_okex_order_internal(
      exchange_order_id=exchange_order_id,
      update_received_timestamp=timestamp,
      status=status,
      fill=fill,
  )
  order = Order(order_id=None,
                product=product,
                price=price_orig,
                qty=qty_orig,
                order_side=order_side,
                order_type=order_type,
                tag=None,
                accepted=True,
                cancel_sent=False,
                internal=internal)
  return status, order


class OkexOrderGatewayInfo(BaseOrderGatewayInfoMutator):
  CurrencyType = OkexCurrency
  ProductType = OkexProduct
  BalanceType = float
  PositionType = None

  def __init__(self, currencies: List[OkexCurrency], products: List[OkexProduct]):
    super().__init__(currencies, products)

  def balance_as_proto(self):
    return AccountBalance(exchange='Okex',
                          market_type="Spot",
                          each_balance=self.get_notNone_balances_internal())

  def update_balance_userinfo(self, data):
    self.set_zero_balances()

    account_balance = OkexPrivateParser.parse_balance(data)
    for balance in account_balance.each_balance:
      currency = OkexCurrency.FromStrCurrency(balance.currency)
      if currency in self.currencies or balance.total > 0:
        self.set_balance(currency, balance.total, internal=balance)

  def update_balance_sub(self, data):
    account_balance = OkexPrivateParser.parse_balance(data)
    for balance in account_balance.each_balance:
      currency = OkexCurrency.FromStrCurrency(balance.currency)
      if currency in self.currencies or balance.total > 0:
        self.set_balance(currency, balance.total, internal=balance)


class OkexOrderGateway(OrderGatewayBase):
  _exchange = 'Okex'
  _market_type = 'Spot'
  _api_version = None

  OGInfoType = OkexOrderGatewayInfo

  def __init__(self,
               currencies: List[OkexCurrency],
               product: Union[List[OkexProduct], OkexProduct],
               config: Config,
               og_config=None,
               logger=None):
    super().__init__()

    self._logger = logger or logging.getLogger(__name__)
    self._og_ready = PositionOrderGatewayReady()
    self._og_info = OkexOrderGatewayInfo(currencies, to_list(product))
    self._og_logger = OkexOrderGatewayLogProvider(gen_og_log_request(config, self))

    self._working_orders = {}
    self._orders_by_exchange_order_id = {}

    self._ioloop = None
    self._ws_client = OkexSpotTradeClient(
        config.access_key,
        config.secret_key, [product.native_symbol for product in self._og_info.products],
        logger=self._logger)
    self._ws_client.ready_callback = self._on_ws_client_ready
    self._ws_client.close_callback = self._on_ws_client_close
    self._ws_client.message_callback = self._on_ws_client_message

    self._rest_client = OkexRestTrade(config.api_key, config.api_secret)
    self._rest_rate_limiter = RestRateLimiter(delay=1)  # sec

    self._order_id_generator = OrderIdGenerator()
    self._order_counter_success = 0

    self._og_config = og_config or OrderGatewayConfig()

    self._unacked_order_periodic_cleaner = None
    self._orders_snapshot_periodic_queryer = None
    self._querying_working_orders = False
    self._last_balance_update_timestamp = None
    self._last_order_info_update_timestamp = None
    self._balance_update_checker = None

  def _debug_print_working_orders(self):
    print_working_orders(self._logger, self)

  def _periodic_balance_update_check(self):
    if self._last_order_info_update_timestamp is None:
      return

    time_diff = abs(self._last_order_info_update_timestamp - self._last_balance_update_timestamp)
    if time_diff > 30 * 1e9:
      self._on_ws_client_close()

  def _add_foreign_order(self, status, order):
    assert isinstance(order.internal.exchange_order_id, int)
    timestamp = get_timestamp()
    order.order_id = self._order_id_generator.gen_order_id(is_foreign=True)
    order.internal.timestamp = timestamp
    order.internal.update_received_timestamp = timestamp
    self._working_orders[order.order_id] = order
    self._orders_by_exchange_order_id[order.internal.exchange_order_id] = order

  def _generate_foreign_order(self, status, order_info):
    order_id = self._order_id_generator.gen_order_id(is_foreign=True)
    fill = FillManager(price_orig=order_info.price,
                       qty_orig=order_info.qty,
                       qty_accum_fill=0,
                       price_avg_fill=0)
    internal = copy.copy(order_info.internal)
    internal.fill = fill
    order = copy.copy(order_info)
    order.order_id = order_id
    order.internal = internal
    self._working_orders[order_id] = order
    self._orders_by_exchange_order_id[order.internal.exchange_order_id] = order
    return order

  def _update_order(self, status, order):
    # TODO(inkyu): change order => order_info / prev_order => order
    if status not in (-1, 0, 1, 2, 3, 4, 5):  # 3 also means cancel in progress.
      self._logger.error("Unknown status code: %s. Order: %s", status, order)
      return

    prev_order = None
    exchange_order_id = order.internal.exchange_order_id
    if exchange_order_id in self._orders_by_exchange_order_id:
      prev_order = self._orders_by_exchange_order_id[exchange_order_id]

    if prev_order is not None and (prev_order.internal.cancel_confirmed
                                   or prev_order.internal.fully_filled):
      if prev_order.internal.status not in (-1, 2):  # TODO(inkyu): This happens. Why?
        self._logger.error('Weird status: %s, %s', prev_order, order)

      if status == -1:
        if prev_order.internal.status == 2:
          self._logger.warning('Receive cancel confirmation after order is fully filled.')
        else:
          # Receive duplicated cancel confirmation is normal, ignore.
          pass
        return

      elif status == 0:
        self._logger.warning(
            'Order is already dead (status=%s), but we received pending order message. %s, fill = %s',
            prev_order.internal.status,
            prev_order,
            prev_order.internal.fill.value)
        return

      elif status == 1:
        if prev_order.internal.status == -1:
          self._logger.warning(
              'Order is already cancel confirmed, but we received partial fill message. %s, fill = %s',
              prev_order,
              prev_order.internal.fill.value)
          # Do not return, proceed to process this message.
        elif prev_order.internal.status == 2:
          self._logger.warning(
              'Received fill message is out of order, fully filled message is received before partial fill message.'
          )
          return
        else:
          self._logger.warning('Inconsistent order status: %s', prev_order)
      elif status == 2:
        if prev_order.internal.status == -1:
          self._logger.warning(
              'Received fully-filled message after cancel confirmation. %s, fill = %s',
              prev_order,
              prev_order.internal.fill.value)
          # Process to process this message
        else:
          self._logger.warning(
              'order status and cancel_confirmation flag are not consistent. %s, %s',
              prev_order,
              order)
          # Process to process this message
      elif status in (3, 4, 5):  # No need to handle
        return

    timestamp = get_timestamp()

    if prev_order is None:
      # Order submission response has not been received, or it is a foreign order.
      prev_order = self._generate_foreign_order(status, order)
      self._logger.debug('Accepted foreign order: %s', prev_order.order_id)

    assert _compare_order(prev_order, order), '%s != %s' % (prev_order, order)
    prev_order.internal.update_received_timestamp = order.internal.update_received_timestamp
    prev_order.internal.status = status
    # status: https://www.okex.com/ws_api.html
    #   -1: cancelled
    #    0: pending
    #    1: partially filled
    #    2: fully filled
    #    3, 4: cancel request in process
    #    5, cancel an already canceled order.
    # TODO(xguo) use enum?
    if status == -1:  # CANCELED
      # generate cancel confirmed
      self._og_logger.gen_order_event_and_log(prev_order, timestamp, OrderEvent.CANCEL_CONFIRMED)
      if prev_order.order_id in self._working_orders:
        del self._working_orders[prev_order.order_id]
      prev_order.internal.cancel_confirmed = True
    elif status == 0:  # PENDING
      # same as submit response
      pass  # handled already
    elif status in (1, 2):  # FILL
      last_fill = prev_order.internal.fill.update_by_accum_fill(
          order.internal.fill.value.price_avg_fill, order.internal.fill.value.qty_accum_fill)
      if last_fill is not None:
        assert last_fill.qty_last_fill > 0 and last_fill.price_last_fill > 0, last_fill
        self._og_logger.gen_order_event_and_log(prev_order, timestamp, OrderEvent.ORDER_FILLED)
        prev_order.internal.fill.log_last_fill(prev_order, self._logger)
        self._publish_fill(prev_order.internal.fill.value.price_last_fill,
                           prev_order.internal.fill.value.qty_last_fill,
                           order)
      else:
        # Last fill can be None if duplicated fill update is received.
        self._logger.warning('last fill is None (status=%s): prev_order=%s, order=%s',
                             status,
                             prev_order,
                             order)
      if status == 2:
        if not prev_order.internal.fill.fully_filled:
          self._logger.warning('Get fully filled status code, but the order is not fully filled.',
                               order)
        if prev_order.order_id in self._working_orders:
          del self._working_orders[prev_order.order_id]
        prev_order.internal.fully_filled = True
    elif status in (3, 4):  # CANCEL IN PROGRESS
      self._og_logger.gen_order_event_and_log(prev_order, timestamp, OrderEvent.CANCEL_ACCEPTED)
    elif status == 5:  # ORDER CANCELED ALREADY
      self._logger.info('Order (%s) canceled already (status: %s)', prev_order.order_id, status)

  @gen.coroutine
  def _query_working_orders_initial_impl(self):
    for product in self._og_info.products:
      for i in range(0, 10):
        try:
          response = yield self._rest_client.order_history(product.native_symbol,
                                                           working_order=True)
          if response.status_code != 200:
            self._logger.error('Invalid status: %s', response)
          else:
            order_history = json.loads(response.content)
            self._on_rest_client_order_history(order_history)
            break
        except json.JSONDecodeError:
          self._logger.exception('JsonDecodeError')
        except requests.exceptions.RequestException:
          self._logger.exception('RequestException raised. Retrying in 5 seconds')
          yield gen.sleep(5)
        except Exception as e:
          self._logger.exception('Error: %s' % e.__class__)
          return
        else:
          self._logger.error('Tried %d times but failed. No querying working orders' % (i + 1))

  @gen.coroutine
  def _query_working_orders_impl(self):
    for product in self._og_info.products:
      try:
        response = yield self._rest_client.order_history(product.native_symbol, working_order=True)
        if response.status_code != 200:
          self._logger.error('Invalid status: %s', response)
        else:
          order_history = json.loads(response.content)
          self._on_rest_client_order_history(order_history)
      except json.JSONDecodeError:
        self._logger.exception('JsonDecodeError')
      except requests.exceptions.RequestException:
        self._logger.exception('RequestException')
      except Exception as e:
        self._logger.exception('Error: %s' % e.__class__)

  @gen.coroutine
  def _initialize_order_gateway(self):
    yield self._query_working_orders_initial_impl()

    if self._working_orders:
      working_orders_str = '\n  '.join([str(order) for order in self._working_orders.values()])
      self._logger.info('Working orders:\n  %s' % working_orders_str)
    else:
      self._logger.info('No working orders')

    self._periodic_query_install()
    self._periodic_balance_update_check_install()

    self._og_ready.set_status(OrderGatewayStatus.READY)
    self._logger.info('Okex OG ready')

  def _periodic_balance_update_check_install(self):
    self._balance_update_checker = PeriodicCallback(self._periodic_balance_update_check, 3000)
    self._balance_update_checker.start()

  def _periodic_query_install(self):
    self._orders_snapshot_periodic_queryer = PeriodicCallback(
        self._periodic_query_working_orders, self._og_config.orders_snapshot_query_period * 1000)
    self._orders_snapshot_periodic_queryer.start()

  @gen.coroutine
  def _periodic_query_working_orders(self):
    if self._querying_working_orders:
      return
    self._querying_working_orders = True
    yield self._query_working_orders_impl()
    self._querying_working_orders = False

  def _on_rest_client_order_history(self, order_history):
    assert order_history['result']
    timestamp = get_timestamp()
    for orderinfo in order_history.get('orders', []):
      status, order = create_okex_order_from_orderinfo(orderinfo, timestamp)
      if order.product not in self._og_info.products:
        continue
      if order.internal.exchange_order_id not in self._orders_by_exchange_order_id:
        self._logger.info('Adding a foreign order: %s', order)
        self._add_foreign_order(status, order)

  def _on_cancel_order_res(self, msg):
    try:
      data = msg['data']
      error_code = data.get('error_code', None)
      if error_code == 1051:
        self._logger.warning('%d: Already completed', error_code)
      elif error_code == 1019:
        self._logger.warning('%d: Already completed', error_code)
      elif error_code is not None:
        self._logger.warning('%d (Unknown)', error_code)
      else:
        if data['result']:
          exchange_order_id = int(data['order_id'])
          if exchange_order_id in self._orders_by_exchange_order_id:
            order = self._orders_by_exchange_order_id[exchange_order_id]
            self._update_order(-1, order)
    except (KeyError, TypeError):
      self._logger.exception('Cancel order error: %s', msg)

  def _on_ws_client_ready(self):
    self._ws_client.user_info()

  def _on_ws_client_close(self):
    self._ws_client.stop(trigger_close_callback=False)
    self._og_ready.set_reinit()
    self._og_info.reset()
    self._og_ready.set_status(OrderGatewayStatus.DISCONNECTED)
    self._orders_snapshot_periodic_queryer.stop()
    self._balance_update_checker.stop()

    self._logger.error('Trade client disconnected, will reconnect soon.')
    self._ws_client = self._ws_client.clone()  # create a new one.
    self._ws_client.start(self._ioloop)

  def _on_ws_client_message(self, msg):
    channel = msg.get('channel', '')

    if channel.startswith('ok_sub_spot_') and channel.endswith('_order'):
      self._last_order_info_update_timestamp = get_timestamp()
      self._on_order_sub(msg)

    elif channel.startswith('ok_sub_spot_') and channel.endswith('_balance'):
      self._last_balance_update_timestamp = get_timestamp()
      self._on_balance_sub(msg)

    elif channel == 'ok_spot_userinfo':
      self._last_balance_update_timestamp = get_timestamp()
      self._on_userinfo_res(msg)

    elif channel == 'ok_spot_orderinfo':
      self._on_orderinfo_res(msg)

    elif channel == 'ok_spot_cancel_order':
      self._on_cancel_order_res(msg)

    elif channel == 'ok_spot_order':
      # uses rest to submit order. do not use.
      pass

    elif channel.startswith('ok_futureusd_'):
      pass

    elif channel.startswith('ok_sub_futureusd_'):
      pass

    else:
      self._logger.info("unhandled message: %s, %s", channel, msg)

  def _on_userinfo_res(self, msg):
    data = msg['data']
    if not data['result']:
      self._logger.error('Error getting userinfo. Trying again. Message: %s' % str(msg))
      self._ws_client.user_info()
      return

    self._og_info.update_balance_userinfo(data)
    self._og_ready.set_balance_ready(True)
    self._og_logger.write_balance(self._og_info.balance_as_proto())
    self._logger.info('OKEX Balances:\n%s'
                      % json.dumps(self._og_info.get_balances(), sort_keys=True, indent=2))

    self._ioloop.add_callback(self._initialize_order_gateway)

  def _on_order_sub(self, msg):
    traderecord = msg['data']
    timestamp = get_timestamp()
    status, order = create_okex_order_from_traderecord(traderecord, timestamp)
    if order.product not in self._og_info.products:
      return
    self._update_order(status, order)

  def _on_orderinfo_res(self, msg):
    data = msg['data']
    timestamp = get_timestamp()
    for orderinfo in data.get('orders', []):
      status, order = create_okex_order_from_orderinfo(orderinfo, timestamp)
      if order.product not in self._og_info.products:
        continue

      self._update_order(status, order)

  def _on_balance_sub(self, msg):
    self._og_info.update_balance_sub(msg['data'])
    self._og_ready.set_balance_ready(True)

    self._og_logger.write_balance(self._og_info.balance_as_proto())

  def _remove_unacked_order(self, order_id, timestamp):
    order = self._working_orders[order_id]
    self._og_logger.gen_flush_unacked_order_event_and_log(order.product.symbol,
                                                          order.order_id,
                                                          timestamp)
    del self._working_orders[order_id]
    self._logger.warning(('Order %s is un-acked more than %s seconds. '
                          'Removing from on-the-fly order list.'),
                         str(order),
                         self._og_config.unacked_order_timeout)
    self._logger.error('Should never reach here!')

  def _cancel_order_with_no_updates(self, order_id):
    order = self._working_orders[order_id]
    self.cancel(order_id, ignore_error=True)
    self._logger.warning('Order %s is has no updates for more than %s seconds. cancel it.',
                         order,
                         self._og_config.unacked_order_timeout)

  def clean_old_unacked_orders(self):
    self._logger.debug('Cleaning old un-acked orders.')
    now_ts = get_timestamp()

    unacked_timeout = self._og_config.unacked_order_timeout * 1e9

    for order in self.get_working_order():
      submit_ts = order.internal.timestamp or 0.0
      if order.accepted:
        update_received_ts = order.internal.update_received_timestamp or 0.0
        if now_ts - update_received_ts > unacked_timeout and not order.cancel_sent:
          self._cancel_order_with_no_updates(order.order_id)
      elif now_ts - submit_ts > unacked_timeout:
        # Remove unacked orders. (Should not happen.)
        self._remove_unacked_order(order.order_id, now_ts)

    cancelled_order_id_list = [
        order.order_id for order in self._working_orders.values() if order.cancel_sent
    ]

    for order_id in cancelled_order_id_list:
      order = self._working_orders[order_id]
      cancel_ts = order.internal.cancel_sent_time
      if not cancel_ts:
        self._logger.error('cancel_sent_time is not set! This should not happend. %s',
                           self._working_orders[order_id])
        continue

      timeout = self._og_config.unacked_cancel_timeout * (10**9)
      if timeout < now_ts - cancel_ts <= 2 * timeout:
        self._logger.info('Cancel elapsed on %s is %s', order_id, now_ts - cancel_ts)
        exchange_order_id = order.internal.exchange_order_id
        self._ws_client.orderinfo(order.product.native_symbol, exchange_order_id)
      elif now_ts - cancel_ts > 2 * timeout:
        self._logger.info('Cancel elapsed on %s is %s. retry cancel', order, now_ts - cancel_ts)
        exchange_order_id = order.internal.exchange_order_id
        self._ws_client.orderinfo(order.product.native_symbol, exchange_order_id)
        self.cancel(order_id, ignore_error=True)

  def start(self, queue, ioloop, flow_sub=None):
    self._ioloop = ioloop
    self._og_ready.set_status(OrderGatewayStatus.INITIALIZING)
    self._ws_client.start(ioloop)

    self._unacked_order_periodic_cleaner = PeriodicCallback(
        self.clean_old_unacked_orders, self._og_config.unacked_order_check_period * 1000)
    self._unacked_order_periodic_cleaner.start()

  def stop(self):
    self._unacked_order_periodic_cleaner.stop()

    # TODO(jaewon): Make proper clean up.
    raise NotImplementedError()

  def get_working_order(self) -> List[Order]:
    return list(self._working_orders.values())

  def submittable(self):
    return self.is_ready() and self._rest_rate_limiter.is_available()

  def submit_impl(self,
                  product: OkexProduct,
                  price: float,
                  qty: float,
                  order_side: OrderSide,
                  order_type: OrderType,
                  order_id: Optional[str] = None,
                  tag=None):
    assert self.is_ready()
    assert order_type == OrderType.LIMIT
    assert order_id is None  # Not supported yet
    assert tag is None

    timestamp = get_timestamp()
    order_id = order_id or self._order_id_generator.gen_order_id()
    self._og_logger.gen_order_request_and_order_event_and_log(product.symbol,
                                                              price,
                                                              qty,
                                                              order_side,
                                                              order_type,
                                                              order_id,
                                                              timestamp)

    fill = FillManager(price_orig=price, qty_orig=qty, qty_accum_fill=0, price_avg_fill=0)
    order = Order(order_id=order_id,
                  product=product,
                  price=price,
                  qty=qty,
                  order_side=order_side,
                  order_type=order_type,
                  tag=None,
                  accepted=False,
                  cancel_sent=False,
                  internal=create_okex_order_internal(fill=fill, timestamp=timestamp))
    self._working_orders[order_id] = order

    self._ioloop.add_callback(self._submit_impl, order_id, order)

  @gen.coroutine
  def _submit_impl(self, order_id, order):
    exchange_order_id = None
    try:
      response = yield self._rest_client.order(order.product.native_symbol,
                                               order.order_side,
                                               order.price,
                                               order.qty)
      if response.status_code != 200:
        self._logger.error('Invalid status code: %s (%s)', response.status_code, response.content)
        if response.status_code in (403, 429):
          self._rest_rate_limiter.set_error(status_code=response.status_code)
      msg = json.loads(response.content)
      exchange_order_id = int(msg['order_id'])
      self._order_counter_success += 1
    except json.JSONDecodeError:
      self._logger.exception('JsonDecodeException: %s', response.content)
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException')
    except (TypeError, KeyError):
      self._logger.error('Wrong order: %s', msg)
    except Exception as e:
      self._logger.exception('Unknown Exception')
      raise e

    timestamp = get_timestamp()
    if exchange_order_id is None:
      if order.accepted:
        self._logger.info('Order already accepted! %s', order.internal.exchange_order_id)
      else:
        self._og_logger.gen_order_response_and_order_event_and_log(order.product.symbol,
                                                                   False,
                                                                   order_id,
                                                                   timestamp)
        try:
          del self._working_orders[order_id]
        except KeyError:
          self._logger.info('Order already removed! %s', exchange_order_id)
    else:
      self._og_logger.gen_order_response_and_order_event_and_log(order.product.symbol,
                                                                 True,
                                                                 order_id,
                                                                 timestamp)
      if exchange_order_id in self._orders_by_exchange_order_id:
        prev_order = self._orders_by_exchange_order_id[exchange_order_id]
        if prev_order.order_id in self._working_orders:
          self._logger.debug('Replace %s => %s', prev_order.order_id, order_id)
          del self._working_orders[order_id]
          del self._working_orders[prev_order.order_id]
          prev_order.order_id = order_id
          prev_order.internal.timestamp = order.internal.timestamp
          self._working_orders[order_id] = prev_order
        else:
          self._logger.warning('order is dead when rest submit response is received! %s\n%s',
                               order,
                               prev_order)
          del self._working_orders[order_id]
        order = prev_order
      else:
        order.accepted = True
        order.internal.exchange_order_id = exchange_order_id
        self._orders_by_exchange_order_id[exchange_order_id] = order
      order.internal.update_received_timestamp = timestamp
      self._og_logger.gen_order_event_and_log(order, timestamp, OrderEvent.ORDER_ACCEPTED)

    self._logger.debug("_on_spot_order. submitted_order/rsp_orders: %s/%s",
                       self._order_id_generator.internal_counter,
                       self._order_counter_success)

  def cancel_impl(self, order_id, *, ignore_error=False):
    assert self.is_ready()
    if order_id not in self._working_orders:
      self._logger.error('No order_id=%s' % order_id)
      return

    order = self._working_orders[order_id]
    if not order.accepted:
      self._logger.error('order_id=%s has not been accepted', order_id)
      return

    timestamp = get_timestamp()
    self._og_logger.gen_cancel_request_and_order_event_and_log(order.product.symbol,
                                                               order.order_id,
                                                               timestamp)

    exchange_order_id = order.internal.exchange_order_id
    if not order.cancel_sent:
      order.cancel_sent = True
      order.internal.cancel_sent_time = timestamp
    ret = self._ws_client.cancel(order.product.native_symbol, exchange_order_id)
    self._og_logger.gen_cancel_response_and_log(order.product.symbol, True, order.order_id)
    return ret

  def cancel_multiple_impl(self, order_ids, *, ignore_error=False):
    for order_id in order_ids:
      self.cancel(order_id, ignore_error=ignore_error)

  def cancel_all_impl(self, *, ignore_error=False):
    for order_id in [key for key in self._working_orders.keys()]:
      self.cancel(order_id, ignore_error=ignore_error)

  def cancel_product_impl(self, product, *, ignore_error):
    order_ids = self.order_ids_by_product(product)
    for order_id in order_ids:
      self.cancel(order_id)

  def order_ids_by_product(self, product):
    native_product = OkexProduct.FromProduct(product)
    res = []
    for order in self._working_orders.values():
      if order.product == native_product:
        res.append(order.order_id)
    return res

  def has_pending_order(self, product):
    native_product = OkexProduct.FromProduct(product)
    for order in self._working_orders.values():
      if order.product == native_product and not order.accepted:
        return True
    return False
